Feat: flux2 dev support - #9234
Conversation
Adds end-to-end support for FLUX.2 [dev] alongside the existing Klein implementation. Dev uses Mistral Small 3.1 (24B) as its sole text encoder instead of Klein's Qwen3, with joint_attention_dim=15360 and the guidance-distilled 32B transformer. Backend - taxonomy: Flux2VariantType.Dev, ModelType.MistralEncoder, ModelFormat.MistralEncoder, MistralVariantType - configs: probe dev via context_in_dim=15360 (main + LoRA); new mistral_encoder.py with Diffusers / Checkpoint / GGUF configs; Main_Diffusers_Flux2_Config accepts Flux2Pipeline class name - loaders: new mistral_encoder.py (AutoModel for Diffusers folder, MistralModel for single-file + GGUF with llama.cpp key conversion). Existing Klein transformer loaders are generic enough for dev - ModelRecordChanges.variant union extended with MistralVariantType Invocations - flux2_dev_model_loader, flux2_dev_text_encoder (Mistral chat-template with FLUX2_DEV_SYSTEM_MESSAGE and layer-stacking 10/20/30), flux2_dev_lora_loader (+ collection variant) - MistralEncoderField on model.py; flux2_denoise / flux2_vae_decode / flux2_vae_encode reused unchanged (already model-agnostic) Frontend - types/hooks/selectors for MistralEncoder, isFlux2DevMainModelConfig, selectFlux2DevDiffusersModels, useMistralEncoderModels - params slice fields flux2DevVaeModel / flux2DevMistralEncoderModel / flux2DevSourceModel + reducers, selectIsFlux2Dev / selectIsFlux2Klein - ParamFlux2DevModelSelect component, wired into AdvancedSettingsAccordion - buildFLUXGraph dev branch with full txt2img / img2img / inpaint / outpaint + multi-reference image editing (same flux_kontext + collect chain as Klein, since Flux2RefImageExtension is model-agnostic) - addFlux2DevLoRAs helper for dev LoRA wiring - zModelType / zModelFormat / zFlux2VariantType extended for mistral_encoder / mistral_small_3_1 / dev - OpenAPI schema regenerated, TS types updated Starter models - FLUX.2 [dev] Diffusers (bf16 + NF4), three GGUFs (Q4/Q6/Q8), Mistral encoder (bf16 + NF4)
Follow-up fixes after first end-to-end run with FLUX.2 [dev] GGUF +
Mistral 3.x GGUF + standalone FLUX.2 VAE.
Frontend
- buildFLUXGraph: wire dev model loader's vae into both flux2_denoise
(required for BN statistics / inpaint) and flux2_vae_decode; missing
edge was raising RequiredConnectionException at runtime
- readiness.ts: variant-aware FLUX.2 readiness check — dev requires
flux2DevVaeModel + flux2DevMistralEncoderModel (or a Dev diffusers
source); Klein keeps Qwen3/VAE check. Threads
hasFlux2DevDiffusersSource through generate + canvas tabs and updates
buildGenerateTabArg / buildCanvasTabArg test helpers
- en.json: noFlux2DevVaeModelSelected, noFlux2DevMistralEncoderModelSelected
Mistral encoder loader (GGUF / single-file)
- Fix "Cannot copy out of meta tensor": llama.cpp conversion produced
`model.*` keys but loader instantiated bare MistralModel (no `model.`
prefix). Add _convert_for_bare_mistral_model to strip the prefix and
drop lm_head before load_state_dict
- _materialize_remaining_meta_tensors: after load_state_dict, replace any
still-meta parameters (norms→ones, others→zeros) and buffers so the
cache→VRAM move can't fail on partial state dicts, with a warning
listing what was missing
- llama.cpp converter: map attn_q_norm/attn_k_norm (Mistral 3.x qk-norm
variants), with ordering before attn_q/attn_k to avoid bad rewrites
Tokenizer / processor fallback
- _load_processor_with_offline_fallback walks a list of sources
(black-forest-labs/FLUX.2-dev tokenizer subfolder, then
mistralai/Mistral-Small-3.1-… and 3.2-…), trying AutoProcessor then
AutoTokenizer for each, cache-first then online. Final error spells
out the three workarounds (install Diffusers folder, set HF_ENDPOINT,
pre-cache the tokenizer)
- flux2_dev_text_encoder: try multimodal `[{type, text}]` chat template
first (PixtralProcessor / Mistral3Processor), fall back to plain
string content (AutoTokenizer), then to manual [INST]…[/INST]
Qwen3 encoder probe strictness
- _get_qwen3_variant_from_state_dict and _get_variant_from_config now
return None / raise NotAMatchError for unknown hidden_size instead of
silently defaulting to qwen3_4b. The old fallback meant any llama.cpp
GGUF causal LM (Mistral, Llama, …) was wrongly classified as Qwen3 —
visible when the Mistral 3.x GGUF was identified as a Qwen3-4B encoder
- Checkpoint / GGUF / Diffusers loaders propagate the strictness
…andlers Upstream Mistral Small 3.1/3.2 (40 layers) produces off-distribution embeddings under FLUX.2's static (10, 20, 30) hidden-state extraction. The joint attention was actually trained against BFL's 30-layer cow-mistral3-small distillation — both Comfy-Org's safetensors and gguf-org's cow GGUFs ship the same 30-layer weights, just packaged differently. - Probing (configs/mistral_encoder.py) now rejects non-cow Mistrals across all three formats (Diffusers / Checkpoint / GGUF) with a clear error. - Loader (load/model_loaders/mistral_encoder.py) extracts the embedded Tekken tokenizer from the `tekken_model` U8 (safetensors) / fp16-per-byte (cow GGUF) tensor via mistral_common, falling back to the BFL HF tokenizer. Removes the INVOKEAI_MISTRAL_TOKENIZER_SOURCE env var. - Starter models: drop upstream Mistral 3.x entries, add Comfy-Org bf16/fp8/fp4 variants alongside the cow GGUFs. - MistralVariantType: drop Small3_1, keep only Cow. - pyproject.toml: add mistral-common dependency. Frontend recall: - Add Flux2DevVAEModel + Flux2DevMistralEncoderModel handlers, disambiguating Klein vs dev via presence of `mistral_encoder` / `qwen3_encoder` metadata fields (both bases are `flux2`). - Wire both into the Recall Parameters panel (hardcoded list was missing them). - Add `metadata.mistralEncoder` i18n key + colocated tests.
…encoders
After studying ComfyUI's `Flux2Tokenizer` / `Mistral3_24BModel` reference
implementation, align the FLUX.2 [dev] text-encoder path with their setup:
- Probing now accepts both 30-layer (cow distillation) and 40-layer (Mistral
Small 3, BFL canonical / upstream) Mistrals. Re-adds `MistralVariantType.Mistral24B`
alongside `Cow`. All three configs (Diffusers / Checkpoint / GGUF) updated.
- Loaders strip `model.norm` (replace with Identity) when the loaded weights
are the 30-layer cow distillation. Matches Comfy's `final_norm=False` for
the pruned variant; for transformers' `MistralModel` the final RMSNorm is
always built but the cow was trained against the raw post-layer-29 state.
- 40-layer loads now log a clear warning that upstream Mistral 3.1 / 3.2 is
NOT what FLUX.2's joint attention was trained against and recommends the
Comfy-Org bf16/fp8/fp4 or gguf-org cow GGUF variants. BFL's canonical
bundled text_encoder is also 40-layer so we don't hard-reject; the warning
is opt-in self-discipline.
- Text encoder invocation switches from `apply_chat_template(messages, ...)`
to a raw text template `[SYSTEM_PROMPT]{sys}[/SYSTEM_PROMPT][INST]{prompt}[/INST]`
fed straight to the tokenizer — byte-for-byte matches Comfy's
`Flux2Tokenizer.llama_template.format(text)`. System prompt now includes
the literal `\n` between "object" and "attribution" Comfy ships.
- `_TekkenChatTemplateAdapter` renamed to `_TekkenRawTextAdapter` and exposes
a `__call__(text, padding_side='left', ...)` interface that Tekken-encodes
the raw string (BOS=1, no EOS) and left-pads with token id 11. Matches
Comfy's `pad_left=True` / `pad_token=11` settings.
Frontend types extended for the new `mistral3_24b` variant
(zMistralVariantType, MODEL_VARIANT_TO_LONG_NAME, schema.ts).
Knip reported 6 unused exports. Each was dead code rather than incomplete wiring, verified against the actual consumers: - Drop the vestigial `flux2DevSourceModel` param end-to-end (state field, default, migration, reducer, action, selector, test). The FLUX graph builder auto-picks the diffusers source itself and never read this param; no UI set it. Mirrors how the Klein path already works. - Delete `selectIsFlux2Klein`; the graph builder computes this locally and only `selectIsFlux2Dev` is consumed. - Un-export `zMistralVariantType`; used only in the local `zAnyModelVariant` union, like `zQwenImageVariantType`. - Delete `selectMistralEncoderModels`; components use the `useMistralEncoderModels` hook instead. - Un-export `isFlux2DevMainModelConfig`; used only within types.ts, like its `isFluxDevMainModelConfig` / `isFlux2Klein9BMainModelConfig` siblings.
|
I've re-verified the branch at 75e9a64 against my review. All 10 findings are correctly fixed, and the cleanup follow-ups from the review body (shared
Only test-coverage note: the dev branch of Resolving the review conversations now; I'll do one more full review pass on the updated branch before signing off. 🤖 Generated with Claude Code |
lstein
left a comment
There was a problem hiding this comment.
Round 2 review at 75e9a64. First, thank you — all 10 round-1 findings were fixed correctly, and the dedup follow-ups came out very clean (details in my earlier comment). This pass covers the fix commits plus a fresh sweep of the whole branch.
4 blockers (🔴) and 6 nice-to-haves (🟡) as inline comments. The one to look at first is the v3→v4 persist migration: as written it wipes the entire params slice for every upgrading user — the fix is one line plus a test-fixture correction.
Cleanup notes (non-blocking, take or leave):
configs/main.py:867+:893-900— with the new layout guard in place, theFlux2Transformer2DModelclass-name entry and the root-config.jsonvariant fallback ("loose transformer-only checkouts") are dead in practice; the comments now describe a layout the guard rejects. Worth dropping both and tightening the comments so nobody re-enables that path.- No identification tests for the three Mistral encoder probes or the new pipeline layout guard —
tests/backend/model_manager/configs/test_qwen3_encoder_config.pyshows the cheap synthetic-dir pattern; this probe has been the most-revised code in the PR and would benefit most. load/model_loaders/mistral_encoder.py:177-189— the three single-key GGUF metadata helpers are dead (they only call each other; production uses the batched reader). Safe to delete.- GGUF header is still parsed twice per text-encoder load (once in
gguf_sd_loader, once in_read_gguf_metadata_values) plus once for the tokenizer submodel — each pass re-walks the 131k-token vocab arrays (~1s each). Folding the metadata read into thegguf_sd_loaderpass would fix it. configs/lora.py:105—_FLUX2_VEC_IN_DIMSis a hand-maintained copy offlux2_variant.py's_VEC_IN_DIMvalues; exporting aFLUX2_VEC_IN_DIMSfrozenset would close the last drift gap the shared module was created to prevent.load/model_loaders/mistral_encoder.py:799-814— the missing-norm re-init loop is fully subsumed by_materialize_remaining_meta_tensors(same init, dtype, device, norm test); its only net effect is suppressing the materialize warning on the checkpoint path while the GGUF path warns for the identical condition. Deleting it deduplicates and unifies the diagnostics._drop_quantization_metadata:352—scale.repeat_interleavematerializes a full weight-shape fp32 scale (3 transient copies vs 2); a reshape+broadcast multiply is a drop-in saving ~0.7-2.7GB on the largest tensors.parsing.test.tsx:34-113— thefakeMainModel/modelRegistryfixtures and the it.each variant axis are dead (the merged handler never readsmetadata.model; the registry key is never looked up), and the comment claims variant disambiguation that no longer exists. The two variant cases run the same test twice.
Checked and deliberately not raised: the Klein LoRA loaders' warn→raise change without a version bump (a dev-variant LoRA config cannot exist pre-PR, so no working workflow changes behavior); the llama.cpp→transformers key converter's replace chain (the trailing-dot patterns are collision-safe and unknown keys pass through unchanged, matching the z_image converter); GGUF reader mmap lifetime on Windows (the raw readers are transient locals; the long-lived-view problem WrappedGGUFReader solves doesn't apply).
🤖 Generated with Claude Code
Blockers: - params migration: seed flux2DevMistralEncoderModel in the v3->v4 step so a genuine v3 blob passes zParamsState.parse() instead of wiping the whole params slice on upgrade; rebuild the migration test fixture as a field-accurate v3 object so it actually covers the regression. - guidance for [dev]: resolve the image's own model in the Guidance metadata parse gate and exempt variant === 'dev' so guidance is displayed/recalled for [dev] (still skipped for Klein); render the guidance slider for FLUX.2 [dev]. - source-model variant guard: require variant == Dev where the dev loader validates its Mistral/VAE source, and reject a [dev] source in the Klein loader — a mismatched pipeline otherwise fails with an opaque matmul error in denoise. - tokenizer offline load: drop the dead root-dir fallback + duplicated pre-try and add a root-directory AutoProcessor step to _load_tokenizer_for_model so processor files alongside the encoder weights load offline. Cleanups: - extract _reinit_inv_freq() with a rope_theta -> rope_parameters/rope_scaling fallback (fixes a latent AttributeError on pinned transformers 5.5, removes a verbatim duplicate). - flux2_dev_lora_collection_loader: replace the base assert with a ValueError that rejects non-FLUX.2 LoRAs, mirroring the Klein collection loader. - diffusers Mistral load: drop the never-run vision_tower/multi_modal_projector (~0.8GB) so they stay out of the cache and VRAM transfers. - clear flux2DevMistralEncoderModel on base switch and intra-flux2 variant switch. - pin mistral-common>=1.5.4,<2 (validated against 1.11.6). - fix contradictory 40-layer docstrings to match the taxonomy/loader story.
Resolve conflicts:
- parsing.test.tsx: keep upstream's new T5EncoderModel handler tests and this
branch's FLUX.2 [dev] guidance variant-gating tests; union the fakeModel type
('mistral_encoder' + 't5_encoder').
- schema.ts / openapi.json: regenerated from the merged backend so both the
FLUX.2 [dev] and T5 encoder additions are present.
…upport Resolves conflicts with upstream video generation (invoke-ai#9163) and Ideogram 4 (invoke-ai#9303). Conflict resolutions: - Kept both new encoder types side by side (mistral_encoder for FLUX.2 [dev], wan_t5_encoder for Wan 2.2) across taxonomy, invocation fields, model manager and node types - isNonCommercialMainModelConfig now covers FLUX dev, FLUX.2 Klein 9B, FLUX.2 dev and Ideogram 4 - Regenerated uv.lock from the merged pyproject (adds imageio/imageio-ffmpeg)
Brings in the round-2 review fixes and the tightened mistral-common pin on top of the local merge of upstream video generation (invoke-ai#9163). Conflict resolutions: - modelSelected: kept the Wan 2.2 auto-default block alongside origin's reworded FLUX.2 variant-switch comment covering both encoder slots
…port # Conflicts: # invokeai/app/services/model_records/model_records_base.py # invokeai/backend/model_manager/configs/factory.py # invokeai/backend/model_manager/taxonomy.py # invokeai/frontend/web/openapi.json # invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts # invokeai/frontend/web/src/features/controlLayers/store/types.ts # invokeai/frontend/web/src/features/modelManagerV2/models.ts # invokeai/frontend/web/src/features/nodes/types/common.ts # invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts # invokeai/frontend/web/src/services/api/hooks/modelsByType.ts # invokeai/frontend/web/src/services/api/schema.ts
…ision main and this branch both shipped _version 4 with different new keys (PiD fields vs the flux2 VAE merge + Mistral encoder slot), so a v4 blob written by either parent would fail zParamsState.parse() after the merge and wipe the whole params slice. Keep main's v3->v4 step verbatim and move the flux2 slot merge + Mistral seed to a new v4->v5 step with conditional seeding for both v4 shapes. Also seed the five Wan component fields in v3->v4: they were added to the schema without a version bump while releases were still writing v3 blobs, so a genuine released-build (v6.13.x) v3 blob fails parse() on them today - same wipe, inherited from main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lstein
left a comment
There was a problem hiding this comment.
I've re-verified the branch at 3e62aa8 against my second review. Thank you for the thorough fix commit (5026c02) — 8 of the 10 findings are correctly and completely fixed, including all the hard parts (the migration seed with a field-accurate test fixture, model-resolving guidance recall, the _reinit_inv_freq extraction validated against transformers 5.5.4, and the vision-tower drop). Two of the fixes need a follow-up, which is why this is another change request rather than an approval. Details below.
Requested changes
🔴 1. The new Klein-loader variant guard broke the VAE-only fallback (regression from the blocker-3 fix)
flux2_klein_model_loader.py calls _validate_diffusers_format on both call sites — the encoder-extraction path (:154) and the VAE-only extraction path (:132). The encoder-side rejection is exactly right, but the 32-channel AutoencoderKLFlux2 is shared between Klein and [dev], and the linear UI deliberately relies on that: buildFLUXGraph.ts falls back to any FLUX.2 diffusers pipeline when only the VAE is needed ("Fall back to any diffusers model if only the VAE is needed"), and isFlux2DiffusersMainModelConfig / readiness don't filter by variant.
Triggering sequence: install a Klein GGUF main + a standalone Qwen3 encoder + a FLUX.2 [dev] diffusers pipeline as the only FLUX.2 diffusers model (no standalone VAE) → readiness enables Invoke → the builder sets qwen3_source_model to the [dev] pipeline for VAE extraction → the new guard raises ValueError claiming the VAE is "incompatible with the Klein transformer" — which worked before this fix, and the message is factually wrong for the VAE.
Suggested fix: apply the variant guard only at the tokenizer/encoder extraction call sites ([dev] loader :153, Klein loader :154), not the VAE ones — or, if you prefer to reject cross-variant VAE sourcing on principle, the frontend readiness + source-selection fallback must be variant-filtered to match, so users get a disabled Invoke button with a reason instead of a runtime error.
🔴 2. The root-directory tokenizer step still fails offline for the exact layout blocker 4 described
The structural cleanup is all there (dead fallback gone, duplicated pre-try gone, ValueError in every except tuple, all three loaders share the helper). But the new step 3 (mistral_encoder.py:676-681) tries AutoProcessor only. With the pinned transformers 5.5.4, AutoProcessor.from_pretrained on a directory whose config.json is model_type: "mistral3" — which is precisely the BFL-style standalone-encoder layout this step targets — raises OSError: Can't load image processor … (it resolves mistral3 to a multimodal processor and requires preprocessor_config.json) before reaching any tokenizer fallback. The exception is swallowed and the ladder falls through to the HF fetch, which raises RuntimeError offline.
Reproduced empirically against the project venv with HF_HUB_OFFLINE=1: a root directory with a mistral3 config.json plus a valid tekken.json (or tokenizer.json + tokenizer_config.json) fails via AutoProcessor, while AutoTokenizer.from_pretrained on the same directory succeeds and returns a working backend. So the tokenizer is loadable — only the loader-class choice loses it. The step only succeeds today when the root config is text-only model_type: "mistral" or a full processor set including preprocessor_config.json is present.
Suggested fix: loop (AutoProcessor, AutoTokenizer) in steps 2 and 3, exactly as _load_tokenizer_from_hf already does (:616). Note if you do: probing showed AutoTokenizer can raise KeyError: 'special_tokens' on a directory containing only tekken.json, so the except tuple there needs KeyError too (or that escapes loudly).
Note on the two commits I just pushed (merge of main + migration v5 bump)
I've pushed 567cba9 (merge of latest main, which brings in the PiD decoder work — the flux2 PiD wiring is adapted into this branch's addFlux2Features helper and verified behavior-identical to main's) and d3c7bde. The second commit fixes a semantic conflict the textual merge couldn't see, and it wasn't your bug: main (after the PiD PR, 802de41) and this branch both bumped the params persist schema to _version: 4 with different new keys (main: pidMode/pidDecoderModel/gemma2EncoderModel/pidSteps, still with kleinVaeModel; this branch: flux2VaeModel + flux2DevMistralEncoderModel). After a plain merge, a v4 blob written by either parent is missing the other side's keys, skips every migration branch, fails zParamsState.parse(), and silently wipes the whole params slice — the same failure mode as blocker 1, reintroduced by the version collision.
The fix keeps main's v3→v4 verbatim and moves this branch's flux2 slot-merge + Mistral seed into a new v4→v5 step with conditional (?? null) seeding for both v4 shapes, with tests covering both. It also seeds the five wan* keys in v3→v4: main added them to the schema without a version bump while v6.13.x releases were still writing v3 blobs, so genuine released-build v3 blobs fail parse on those keys today (empirically confirmed — inherited from main, needs the same fix there).
Verified fixed (no action needed)
- Migration seed (blocker 1) —
flux2DevMistralEncoderModel = nullseeded in the migration; the rebuilt fixture genuinely covers the regression (deleting the seed fails the test). ✅ - Guidance for [dev] (blocker 2) — the parse gate resolves the image's own model and exempts
variant === 'dev'; Klein still skips; slider renders for dev only via the same predicate the graph builder uses, so visibility and consumption can't drift; generate → metadata → recall → regenerate round-trips. Uninstalled-model dev images fall back to skipping guidance — reasonable fail-safe. ✅ - Encoder-side variant guards (blocker 3) — the reported Klein-encoder-into-dev-transformer path (and mirror) is closed on every reachable route with clear
ValueErrors, and the linear UI filters dev sources by variant. (VAE-path over-reach above is the only issue.) ✅ - Tokenizer structure (blocker 4) — dead fallback and duplicate pre-try removed,
ValueErrorcaught, single helper used by all three loaders. (AutoProcessor-only gap above is the only issue.) ✅ - Collection LoRA base check —
ValueErrorbefore dedup/config fetch; FLUX.1 rejected; round-1 Klein-variant rejection intact; Klein side symmetric. ✅ - Mistral slot clearing — cleared on base switch (with toast accounting) and intra-flux2 variant switch, including when the Klein encoder is unset; recall path covered via
modelSelected. ✅ rope_theta— single_reinit_inv_freqwith therope_parameters/rope_scalingfallback; verified on transformers 5.5.4 thatrope_thetaset viaMistralConfig(rope_theta=…)lands exactly where the fallback reads it. ✅- mistral-common pin —
>=1.5.4,<2in the right block, lock resolves 1.11.6, and both private-API usages (_special_tokens_reverse_vocab,Tekkenizer.encode(s, bos, eos)) verified present in-range with defensive fallbacks. ✅ - Vision tower drop —
setattr(model, …, None)removes the tower/projector fromstate_dict()/named_parameters(), so cache accounting and VRAM transfers exclude it;.language_modelaccess and index-based extraction unaffected. Residual, non-blocking: the sibling full-pipeline path (flux.pyFlux2DiffusersModel._load_model, used for diffusers mains andmistral_source_model) still loads and carries the ~0.8 GB tower — worth the same treatment in a follow-up. ✅ - 40-layer docs — all four sources now agree on the operational contract (accepted, warned, indices 10/20/30, final norm kept for 40-layer / stripped for cow). Nit, non-blocking:
starter_models.pysays BFL's own 40-layer has weaker adherence due to index depth, while the loader warning implies only non-BFL 40-layer weights degrade — pick one story. ✅
Pre-existing, out of scope for this PR (noting for completeness): the workflow editor can still feed a Klein 9B-family source into a 4B main (no cross-Klein variant check on the source-extraction path — same wrong-width class, linear UI is protected); the Klein loader never validates its main model's variant.
🤖 Generated with Claude Code
…n, widen the tokenizer ladder The FLUX.2 loaders ran one validator on both the VAE- and the encoder-extraction call site, so the cross-variant check also rejected VAE-only sourcing. Klein and [dev] share the same 32-channel AutoencoderKLFlux2 and the linear UI relies on that -- buildFLUXGraph falls back to any FLUX.2 diffusers pipeline when only the VAE is needed, and readiness does not filter by variant. A Klein GGUF main plus a standalone Qwen3 encoder plus a [dev] pipeline as the only diffusers model therefore hit a ValueError behind an enabled Invoke button. Split the validator: format-only for the VAE path, format + variant for the encoder path. The Mistral tokenizer ladder's local-directory rungs tried AutoProcessor only. On transformers 5.5.4 that raises OSError for a mistral3 config.json without preprocessor_config.json -- exactly the BFL-style standalone-encoder layout the rungs target -- so the ladder fell through to the HF fetch and failed offline. Both rungs now loop (AutoProcessor, AutoTokenizer), with KeyError in the except tuple for tekken-only directories.
FLUX.2 [dev] recorded its Mistral text encoder as an undeclared extra key, relying on the node's `extra='allow'`, while the Klein counterpart `qwen3_encoder` is a proper field. Declare it so it lands in the OpenAPI schema and is typed in the frontend instead of `unknown`. Also bumps the node version, which was left at 2.1.0 across several model integrations that widened the node: `ideogram4_caption` (invoke-ai#9303) and the generation modes for FLUX.2, Anima, Qwen-Image, Ideogram 4, Wan, Krea-2 and Ernie. All changes are additive, so this is a minor bump - saved workflows carrying a core_metadata node now auto-update to the current field set on load rather than silently keeping a stale one.
Conflicts: - addRegions.ts: adopted main's RegionalPositiveConditioning alias and added flux2_dev_text_encoder to it - schema.ts/openapi.json: regenerated from the merged backend Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Main's invoke-ai#9428 marked every text-encoder node idle_gpu_offloadable and added a registry guard asserting the marker on all *_text_encoder nodes; the merge brought that guard onto this branch where flux2_dev_text_encoder (which neither parent knew about) fails it. The flag alone would be wrong: the marker's contract is that the saved conditioning is CPU-backed, because the borrowed GPU's pool lock is released the moment the node returns. Move the Mistral embeds to CPU before save (the placeholder clip_embeds follows their device), add the marker, bump to 1.0.1, and add the same output-device regression test the Klein encoder has. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lstein
left a comment
There was a problem hiding this comment.
Round 4 review (head c2b7778)
Thanks for the quick turnaround on both round-3 blockers. I re-verified each fix in code and with fresh-context adversarial passes (including mutation-testing your new tests — they do catch reverts). Summary:
- Round-3 blocker 1 (cross-variant guard also rejected VAE-only sourcing): fixed correctly. The split is right, every source-based encoder extraction goes through
_validate_encoder_source, no VAE branch retains variant logic, and the linear UI can't wire a cross-variant source into an encoder slot in any main-format × standalone-VAE × standalone-encoder combination I enumerated. - Round-3 blocker 2 (AutoProcessor-only local rungs): the offline gap itself is fixed — both local rungs now loop
(AutoProcessor, AutoTokenizer). Unfortunately the widened AutoTokenizer rung introduces two new problems, one of which silently corrupts conditioning. Details below; both reproduced empirically against transformers 5.5.4 with real files, not hypothesized. - The
core_metadatamistral_encoderdeclaration + 2.2.0 bump looks good.
Housekeeping: I pushed two commits
7179712— merge of currentmain. Conflicts:addRegions.ts(main refactored the posCond unions into a sharedRegionalPositiveConditioningalias for Krea-2 regional prompting; I addedflux2_dev_text_encoderto the alias and took main's shape), andschema.ts/openapi.json(regenerated from the merged backend rather than hand-merged).c2b7778— the merge surfaced a semantic conflict git can't see: main's #9428 added a registry guard asserting every*_text_encodernode declaresidle_gpu_offloadable=True, andflux2_dev_text_encoder(which neither parent knew about) failed it, so python-tests would have gone red. The flag alone would have been wrong — the marker's contract is that saved conditioning is CPU-backed, because the borrowed GPU's pool lock is released the moment the node returns. I mirrored the Klein encoder:detach().to("cpu")before save, marker added, version 1.0.1, plus the same output-device regression test Klein has.tests/appis fully green at head (2161 passed), as are the frontend suite (1736) and lints.
Blocker 1: AttributeError escapes the ladder and crashes the whole load
_TOKENIZER_LOAD_ERRORS = (OSError, EnvironmentError, ValueError, KeyError) (mistral_encoder.py:623) doesn't cover a failure mode of the very rung you added. In transformers 5.5.4, AutoTokenizer.from_pretrained on a directory whose tokenizer_config.json names a tokenizer_class unknown to 5.5.4 hits tokenizer_class_from_name(...) → None and raises AttributeError: 'NoneType' object has no attribute 'from_pretrained' (tokenization_auto.py:723 has no None-guard).
Triggering sequence (reproduced): encoder dir with config.json (model_type: "mistral3"), tekken.json, and a tokenizer_config.json containing "tokenizer_class": "MistralCommonTokenizer" — exactly what transformers 4.5x's MistralCommonTokenizer.save_pretrained writes. Rung 3's AutoProcessor raises OSError (caught), AutoTokenizer raises AttributeError → escapes _try_load_tokenizer_from_dir → _load_tokenizer_for_model crashes instead of falling through to the HF rung. Before this commit the same directory worked whenever rung 4 could succeed.
Given the ladder's whole contract is graceful fallthrough, I'd catch broadly in _try_load_tokenizer_from_dir (add AttributeError, or catch Exception and log) — an exotic exception from a probe rung should never be able to kill the load while a working fallback remains.
Blocker 2: root tekken.json + config.json now loads MistralCommonBackend, which tokenizes the template markers wrong — silently
The commit comment assumes tekken-carrying directories fail with KeyError: 'special_tokens'. That's only true when config.json is absent. With config.json (model_type: "mistral3") + a real tekken.json — the layout of an official mistralai Mistral-Small download, and the standalone-encoder layout these loaders explicitly accept — AutoTokenizer succeeds and returns MistralCommonBackend (mistral-common is a hard dependency, so this holds in every standard install).
That object BPE-encodes [SYSTEM_PROMPT]/[INST] as literal text instead of splicing them as single Tekken special ids. Verified against this module's own _TekkenRawTextAdapter on the same prompt: reference ends ..., 1, 17, 33153, 5117, 18, 3, 1097, 7990, 4 (markers = ids 17/18/3/4); MistralCommonBackend produces 1, 1091, 101289, 58343, ... (literal [, SY, STEM…). Every structural token is off-distribution, the encode "works", and generations silently degrade. Before this commit, that directory fell through to rung 4 and got the correct BFL processor whenever online/cached — so this regresses the online case too, and offline it trades an actionable RuntimeError for silent corruption.
Your ladder tests missed it because the fixture's tekken.json is a small fake that raises during parse; a real one parses fine and short-circuits.
Suggested fix: the ideal handling for a root tekken.json already exists in this file — wrap it with _TekkenRawTextAdapter (same as the embedded-tekken rung) before ever handing the directory to AutoTokenizer; and/or treat a mistral-common-backed result from AutoTokenizer as "not usable, keep falling". Either way, please add a fixture with a structurally valid tekken model file so the test exercises the success path, not just the raise path.
Non-blockers
- The [dev]-side justification of the guard split overclaims, and the [dev] half is UI-dead. "buildFLUXGraph falls back to any FLUX.2 diffusers pipeline when only the VAE is needed" is true only of the Klein branch. The dev branch selects from dev-only pipelines (
selectFlux2DevDiffusersModels) and readiness requires a dev diffusers source, so the config yourtest_dev_loader_extracts_vae_from_a_klein_pipelineproves the backend accepts (dev GGUF + standalone Mistral encoder + Klein diffusers as the only VAE source) still shows a disabled Invoke button. Safe direction, but either relax the dev builder/readiness VAE-sourcing to mirror Klein or trim the docstrings to the Klein case. _validate_encoder_sourcein the Klein loader only rejectsDev, not Klein-family mismatches, while its comment claims the opaque-matmul class is closed. Klein 4B GGUF main + Klein 9B diffusers asqwen3_source_model(workflow editor) passes the guard and hits exactly that matmul error; the standalone-encoder path (_validate_qwen3_encoder_variant) and the frontend both enforce the family match — only the source path doesn't. Pre-existing, but worth closing while you're here: check the Klein→Qwen3 family map in the source path too. Related asymmetry: the dev guard rejects any non-dev variant, the Klein guard only dev — a future third FLUX.2 variant would be rejected on one side and silently accepted on the other.
Requesting changes for the two tokenizer-ladder blockers.
…silently mis-encoding Tekken Round-4 review blockers, both reproduced against transformers 5.5.4 with real files before fixing. 1. An AttributeError from a probe rung killed the whole load. `AutoTokenizer.from_pretrained` on a directory whose `tokenizer_config.json` names a `tokenizer_class` the installed transformers does not know resolves that class to None and dereferences it without a guard — exactly the layout `MistralCommonTokenizer.save_pretrained` writes. That is not in `_TOKENIZER_LOAD_ERRORS`, so it escaped `_try_load_tokenizer_from_dir` and crashed a load the HF rung would have completed. The probes now catch broadly; the expected-error tuple only selects the log level, so an unexpected failure is still logged loudly instead of being swallowed. 2. A root `tekken.json` next to `config.json` did not fail in `AutoTokenizer` — it resolved to a mistral-common-backed tokenizer that BPE-encodes `[SYSTEM_PROMPT]`/`[INST]` as literal text instead of splicing them as single Tekken ids. The encode "worked" and conditioning was silently off-distribution. Fixed on two independent paths: the ladder now reads a standalone `tekken.json` itself, ahead of the transformers probes, and any mistral-common-backed result is re-wrapped in `_TekkenRawTextAdapter` through its underlying `MistralTokenizer` rather than used as-is. The vocab is fine — only its `__call__` is wrong — so re-wrapping beats discarding, which would have traded silent corruption for an offline RuntimeError. Verified: the re-wrapped ids are identical to the reference adapter's. Also closes both non-blockers: - `_validate_encoder_source` in the Klein loader rejected only [dev], so a Klein 9B pipeline passed as `qwen3_source_model` for a Klein 4B transformer and hit the very matmul error the guard exists to prevent (the frontend and the standalone-encoder path both enforce the family match; the workflow editor's source field was the only way in). It is now an allowlist keyed on a shared `_KLEIN_TO_QWEN3_VARIANT` map — mirroring the frontend's `KLEIN_TO_QWEN3_VARIANT_MAP` — and checks the source's Qwen3 family against the main model, so a future third FLUX.2 variant fails closed on the Klein side too, not just on [dev]. `_validate_qwen3_encoder_variant` shares that map and now uses `getattr` instead of `hasattr`, which turned a None variant into an AttributeError in the error path rather than the intended ValueError. - The [dev] loader's `_validate_diffusers_format` docstring claimed the linear UI relies on the permissive VAE path. That holds for Klein, but the [dev] builder sources from dev-only pipelines and readiness gates on one, so there the cross-variant VAE case is reachable through the workflow editor only. The justification now states what actually holds: the 32-channel AutoencoderKLFlux2 is shared (the repo ships the Klein-sourced `flux2_vae` as a dependency of every [dev] GGUF starter), and `mistral_source_model` is not variant-filtered in the editor. Tests: a structurally valid Tekken fixture, so the ladder exercises the success path rather than only the raise path the previous fake produced; regression tests for both blockers on the directory and HF rungs; Klein-family coverage including same-family acceptance and the standalone-encoder guard's negative path, which had no coverage at all. All new tests mutation-verified — reverting the broad catch, the tekken rung, the re-wrap, the family check, or the allowlist each fails at least one. tests/app + tests/backend/model_manager: 3010 passed. The 9 failures are the pre-existing network-dependent ones in test_model_install / test_load_api / test_download_queue.
Summary
Adds end-to-end support for FLUX.2 [dev] alongside the existing FLUX.2 Klein implementation. Dev uses a Mistral Small 3 text encoder (hidden 5120 →
joint_attention_dim15360, hidden states sampled at layers 10/20/30) instead of Klein's Qwen3. It shares the 32-channelAutoencoderKLFlux2VAE and the 4D-RoPE sampling backend with Klein, so most existing infrastructure is reused — only the Mistral encoder loaders/configs and the Dev graph wiring are new.Two Mistral encoder packagings are auto-detected: the 40-layer Mistral that ships in the
black-forest-labs/FLUX.2-devdiffusers repo, and the 30-layercow-mistral3-smalldistillation (Comfy-Org safetensors, gguf-org GGUFs). Single-file and GGUF encoders embed the Tekken tokenizer, so no separate tokenizer download is needed.What works
Limitations / notes
tokenizer/) fall back to fetchingblack-forest-labs/FLUX.2-dev:tokenizerfrom HF; offline with no HF cache this errors with documented workarounds.isNonCommercialMainModelConfig).How to test
Automated:
# backend (fast, no model load) uv run --extra cuda pytest tests/test_imports.py tests/model_identification/test_identification.pypyproject.tomlgainedmistral-common;uv.lockis regenerated (uv lock --lockedpasses).End-to-end:
gguf-org/flux2-dev-ggufquant — ~20 GB for Q4_K_M — or the fullblack-forest-labs/FLUX.2-devdiffusers folder), the FLUX.2 VAE, and a Mistral encoder (Comfy-Org fp8 recommended, or a Q6/Q8 cow GGUF).Loaded embedded Tekken tokenizer from <file>(no HF fetch).Checklist
What's Newcopy (if doing a release after this PR)Closes #8668